using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using System.Threading;
using DragonAwakening.Core.World;

namespace DragonAwakening.Game
{
    public class SceneManager3D
    {
        private static bool TextureEnabled = true;
        
        private static Vector3 CameraPosition = new Vector3(0, 00, 65);
        private static Vector3 CameraTarget = new Vector3(0, 0, -10000);

        private static float FieldOfView = MathHelper.ToRadians(45.0f);

        private static Vector3 CubeSize = new Vector3(130, 100, 130);

        private GraphicsDevice device;

        private Matrix worldMatrix;
        private Matrix viewMatrix;
        private Matrix projectionMatrix;
        private Matrix worldViewProjMatrix;

        private BasicEffect effect;
        
        private TextureManager textureManager;

        private SceneWallData[] sceneInfo;
        private short sceneInfoCount;

        private SceneWallData[] skybox;

        private GameInstance game;

        private Texture2D[] skyboxTextures;
        private Model skyboxModel;

        private static Vector3[] Cube = new Vector3[] 
        {
            new Vector3(-1f, -1f, -1f), 
            new Vector3(-1f,  1f, -1f),
            new Vector3( 1f, -1f, -1f),
            new Vector3( 1f,  1f, -1f),
            new Vector3(-1f, -1f,  1f), 
            new Vector3(-1f,  1f,  1f),
            new Vector3( 1f, -1f,  1f),
            new Vector3( 1f,  1f,  1f)
        };

        private static short[][] CubeIndex = new short[][]
        {
            new short[] { 0, 1, 2, 3 },
            new short[] { 4, 5, 6, 7 },
            new short[] { 4, 5, 0, 1 },
            new short[] { 2, 3, 6, 7 },
            new short[] { 5, 7, 1, 3 },
            new short[] { 0, 2, 4, 6 }
        };

        /// <summary>
        /// Scene wall.
        /// </summary>
        private class SceneWallData
        {
            public static int numberOfVertices = 4;

            public static short[] primitiveIndices = new short[4] { 0, 1, 2, 3 };
            public static int numberOfPrimitives = 2;

            public static short[] primitiveIndicesLines = new short[6] { 2, 0, 1, 2, 3, 1};
            public static int numberOfPrimitivesLines = 5;

            public static PrimitiveType primitiveType;

            public VertexPositionNormalTexture[] vertices = new VertexPositionNormalTexture[4] { 
                new VertexPositionNormalTexture(), 
                new VertexPositionNormalTexture(), 
                new VertexPositionNormalTexture(), 
                new VertexPositionNormalTexture() 
            };

            public Texture2D texture;

            private Effect effect;

            /// <summary>
            /// Constructs the scene wall.
            /// </summary>
            public SceneWallData(Effect effect)
            {
                this.effect = effect;
            }

            /// <summary>
            /// Fills the data with vector information.
            /// </summary>
            public void Fill(Vector3[] p, Texture2D texture)
            {
                this.vertices[0].Position = p[0];
                this.vertices[0].TextureCoordinate = new Vector2(0, 1);

                this.vertices[1].Position = p[1];
                this.vertices[1].TextureCoordinate = new Vector2(0, 0);

                this.vertices[2].Position = p[2];
                this.vertices[2].TextureCoordinate = new Vector2(1, 1);

                this.vertices[3].Position = p[3];
                this.vertices[3].TextureCoordinate = new Vector2(1, 0);

                Vector3 side1 = this.vertices[0].Position - this.vertices[2].Position;
                Vector3 side2 = this.vertices[1].Position - this.vertices[2].Position;
                Vector3 normal = Vector3.Cross(side1, side2);

                this.vertices[0].Normal += normal;
                this.vertices[1].Normal += normal;
                this.vertices[2].Normal += normal;
                this.vertices[3].Normal += normal;

                this.texture = texture;
            }

            /// <summary>
            /// Draws the scene wall
            /// </summary>
            public void Draw()
            {
                effect.Parameters["Texture"].SetValue(texture);
                foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                {
                    pass.Apply();

                    if (TextureEnabled)
                    {
                        effect.GraphicsDevice.DrawUserIndexedPrimitives(primitiveType, vertices, 0, numberOfVertices, primitiveIndices, 0, numberOfPrimitives);
                    }
                    else
                    {
                        effect.GraphicsDevice.DrawUserIndexedPrimitives(primitiveType, vertices, 0, numberOfVertices, primitiveIndicesLines, 0, numberOfPrimitivesLines);
                    }
                }
            }
        }

       // Constructs the scene manager.
        public SceneManager3D(TextureManager textureManager, GraphicsDevice device, GraphicsDeviceManager graphics, GameInstance game, ContentManager content)
        {
            // Initialize dependencies
            this.textureManager = textureManager;
            this.device = device;
            this.game = game;

            // Initialize effect
            this.effect = new BasicEffect(device);
            this.effect.TextureEnabled = SceneManager3D.TextureEnabled;
            this.effect.FogEnabled = true;
            this.effect.FogStart = 0;
            this.effect.FogEnd = 1000;
            this.effect.FogColor = Color.DarkGray.ToVector3();
            this.effect.LightingEnabled = TextureEnabled;
            this.effect.AmbientLightColor = Color.White.ToVector3();

            float nearClippingDistance = 0.1f;
            float farClippingDistance = 1000f;
            float aspectRatio = (float)graphics.PreferredBackBufferWidth / (float)graphics.PreferredBackBufferHeight;

            Vector3 cameraUp = Vector3.UnitY;

            worldMatrix = Matrix.Identity; //Matrix.CreateWorld(new Vector3(0, 20, 0), Vector3.Forward, Vector3.UnitY);
            projectionMatrix = Matrix.CreatePerspectiveFieldOfView(FieldOfView, aspectRatio, nearClippingDistance, farClippingDistance);
            viewMatrix = Matrix.CreateLookAt(CameraPosition, CameraTarget, cameraUp);
            
            worldViewProjMatrix = worldMatrix * viewMatrix * projectionMatrix;

            // Initialize rasterizer state
            RasterizerState rs = new RasterizerState();
            rs.CullMode = CullMode.None;
            device.RasterizerState = rs;

            device.BlendState = BlendState.AlphaBlend;

            // Initialize scene info buffer
            sceneInfo = new SceneWallData[200];
            for (int i = 0; i < sceneInfo.Length; i++)
            {
                sceneInfo[i] = new SceneWallData(effect);
            }

            SceneWallData.primitiveType = SceneManager3D.TextureEnabled ? PrimitiveType.TriangleStrip : PrimitiveType.LineStrip;

            this.skyboxModel = content.Load<Model>("Skybox\\skybox");
            this.skyboxTextures = new Texture2D[this.skyboxModel.Meshes.Count];

            int j = 0;
            foreach (ModelMesh mesh in this.skyboxModel.Meshes)
            {
                foreach (BasicEffect currentEffect in mesh.Effects)
                {
                    this.skyboxTextures[j++] = currentEffect.Texture;
                }
            }

            foreach (ModelMesh mesh in this.skyboxModel.Meshes)
            {
                foreach (ModelMeshPart meshPart in mesh.MeshParts)
                {
                    meshPart.Effect = effect.Clone();
                }
            }
        }

        /// <summary>
        ///  Draws the scene
        /// </summary>
        public void DrawScene(GraphicsDevice device)
        {
            // Initialize camera position & transformation matrixes
            BuildSceneInfo();

            DrawSkybox();

            // Draw scene info
            effect.Parameters["WorldViewProj"].SetValue(worldViewProjMatrix);
            for(int index = 0; index < sceneInfoCount; index++)
            {
                sceneInfo[index].Draw();
            }
        }

        /// <summary>
        /// Builds the scene
        /// </summary>
        private void BuildSceneInfo()
        {
            float halfHeight = CubeSize.Y / 2.0f;
            float halfWidth = CubeSize.X / 2.0f;
            float halfDistance = CubeSize.Z / 2.0f;

            var wallTexture = this.textureManager.Get("wall");
            var forestTexture = this.textureManager.Get("forest");
            var grassTexture = this.textureManager.Get("grass");

            int index = 0;

            for (int y = 6; y >= 0; y--) 
            {
                for (int x = -3; x < 4; x++)
                {
                    Point map = new Point(0, 0);

                    switch (this.game.direction)
                    {
                        case Sides.North: map = new Point(x, -y); break;
                        case Sides.South: map = new Point(-x, y); break;
                        case Sides.West: map = new Point(-y, -x); break;
                        case Sides.East: map = new Point(y, x); break;
                    }

                    map = map.Add(this.game.position);

                    var cell = this.game.world.GetCell(map);
                    if (cell != World.empty)
                    {
                        foreach (var side in new[] { Sides.North, Sides.Floor, Sides.Sky, Sides.West, Sides.East, Sides.South })
                        {
                            Sides projectedSide = SideHelper.RotateSide(side, this.game.direction);
                            int textureId = cell.GetWall(projectedSide);
                            if (textureId > 0)
                            {
                                Texture2D texture = null;
                                if (projectedSide == Sides.Floor)
                                { 
                                    texture = grassTexture;
                                }
                                else
                                {
                                    switch(textureId)
                                    {
                                        case 1:
                                            texture = forestTexture; break;
                                        case 2: 
                                            texture = wallTexture; break;
                                    }
                                }

                                this.CreateWall(x, y, side, texture, sceneInfo[index]);
                                index++;                                
                            }
                        }
                    }
                }
            }
            
            for (int y = 6; y >= 0; y--) 
            {
                for (int x = -3; x < 4; x++)
                {
                    Point map = WorldHelper.RotateOffset(new Point(x, y), this.game.direction);
                    map = map.Add(this.game.position);

                    var cell = this.game.world.GetCell(map);
                    if (cell != World.empty)
                    {
                        if (cell.HasCreatures())
                        {
                            foreach (var creatureId in cell.Creatures)
                            {
                                var creature = CreatureManager.GetCreatureById(creatureId);
                                if (creature != null)
                                {
                                    var texture = this.textureManager.textures[creature.TextureId];

                                    // draw big creature
                                    var middleGround = new Vector3(x * CubeSize.X, -1 * CubeSize.Y / 2, -1 * y * CubeSize.Z + CubeSize.Z / 2);
                                    Vector3[] data = new Vector3[] 
                                    { 
                                        new Vector3(middleGround.X - CubeSize.X / 2, middleGround.Y, middleGround.Z),
                                        new Vector3(middleGround.X - CubeSize.X / 2, middleGround.Y + CubeSize.Y, middleGround.Z),
                                        new Vector3(middleGround.X + CubeSize.X / 2, middleGround.Y, middleGround.Z),
                                        new Vector3(middleGround.X + CubeSize.X / 2, middleGround.Y + CubeSize.Y, middleGround.Z)
                                    };

                                    sceneInfo[index++].Fill(data, texture);
                                }
                            }
                        }
                    }
                }
            }

            sceneInfoCount = (short)index;
        }

        private void DrawSkybox()
        {
            var cameraPosition = Vector3.Zero;
            var cameraUp = Vector3.UnitY;
            var cameraTarget = -Vector3.UnitZ;

            Matrix rotation = Matrix.CreateRotationY((float)(Math.PI / 2 * (float)this.game.direction));
            cameraTarget = Vector3.Transform(cameraTarget, rotation);
            
            var vm = Matrix.CreateLookAt(cameraPosition, cameraTarget, cameraUp);

            SamplerState ss = new SamplerState();
            ss.AddressU = TextureAddressMode.Clamp;
            ss.AddressV = TextureAddressMode.Clamp;

            device.SamplerStates[0] = ss;

            DepthStencilState dss = new DepthStencilState();
            dss.DepthBufferEnable = false;
            device.DepthStencilState = dss;

            //device.RasterizerState = new RasterizerState() { CullMode = CullMode.None };
            //device.BlendState = new BlendState();

            Matrix[] skyboxTransforms = new Matrix[skyboxModel.Bones.Count];
            skyboxModel.CopyAbsoluteBoneTransformsTo(skyboxTransforms);

            int i = 0;
            foreach (ModelMesh mesh in skyboxModel.Meshes)
            {
                foreach (Effect currentEffect in mesh.Effects)
                {
                    Matrix w = skyboxTransforms[mesh.ParentBone.Index] * Matrix.CreateTranslation(Vector3.Zero);
                    Matrix wvp = w * vm * this.projectionMatrix;
                    currentEffect.Parameters["Texture"].SetValue(this.skyboxTextures[i++]);
                    currentEffect.Parameters["WorldViewProj"].SetValue(wvp /* rotation*/);
                }

                mesh.Draw();
            }

            dss = new DepthStencilState();
            dss.DepthBufferEnable = true;
            device.DepthStencilState = dss;
        }

        private void CreateWall(int x, int y, Sides side, Texture2D texture, SceneWallData data)
        {
            short[] vect = null;

            switch (side)
            {
                // back
                case Sides.North: vect = CubeIndex[0] ; break;
                // front
                case Sides.South: vect = CubeIndex[1] ; break;
                // left
                case Sides.West: vect = CubeIndex[2]; break;
                // right
                case Sides.East: vect = CubeIndex[3]; break;
                // top
                case Sides.Sky: vect = CubeIndex[4]; break;
                // bottom
                case Sides.Floor: vect = CubeIndex[5]; break;
            }

            Vector3 scale = new Vector3(CubeSize.X / 2, CubeSize.Y / 2, CubeSize.Z / 2);
            Vector3 move = new Vector3(x * CubeSize.X, 0, -1 * y * CubeSize.Z);

            Vector3[] p = new Vector3[] 
            {
                Cube[vect[0]].Scale(scale).Move(move),
                Cube[vect[1]].Scale(scale).Move(move),
                Cube[vect[2]].Scale(scale).Move(move),
                Cube[vect[3]].Scale(scale).Move(move)
            };

            data.Fill(p, texture);
        }
    }

    public static class VectorExtensions
    {
        public static Vector3 Move(this Vector3 src, Vector3 move)
        {
            return new Vector3(src.X + move.X, src.Y + move.Y, src.Z + move.Z);  
        }

        public static Vector3 Scale(this Vector3 src, Vector3 scale)
        {
            return new Vector3(src.X * scale.X, src.Y * scale.Y, src.Z * scale.Z);
        }
    }
}
